Skip to content

fix(mcp): an action on a sys_* object whose author exposed it to AI is listed and run over MCP, behind every gate - #27940

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-qid1f1
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-qid1f1

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Requested by Sales · project thread

Fixes #27681

Clause-②: yes (widening: MCP list_actions / run_action / resume_run now admit a sys_* action that declares ai.exposed: true; McpActionSummary gains the optional aiExposed)
Metadata-protocol: none
Ruling-ref: 6513869839

维护者速读

  • 改了什么:系统对象(sys_*)上的动作,只要作者在动作上声明了 ai: { exposed: true, description },就能经 MCP 被列出和执行;未声明的仍按对象名拒绝。
  • 为什么改:裁决 B(6513869839)——人和 agent 用同一套系统,暴露与否按动作逐条声明,不再按对象名前缀一刀切;REST 早已对同一调用方开放这些动作。
  • 风险与回滚:今天平台没有任何 sys_* 动作声明 ai 块,所以合入当天行为不变;存储元数据表(带凭据)照旧对 MCP 关闭;回滚即 revert 本 PR。权限、开关、确认、RLS 各闸照旧在后面跑。
  • 先看:packages/runtime/src/action-execution.ts 的 systemObjectActionExposureError;packages/mcp/src/mcp-http-tools.ts 的 isListableAction 与 run_action 预检;packages/runtime/src/domains/mcp.ts 的 listActions / resume 两处。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run src/mcp-system-object-action-exposure.test.ts,预期 11 条全绿(未声明的被拒、声明的过每道闸、存储元数据族表照旧拒绝)。
  • 需要你做的:无,席位落地。

What changes

Maintainer ruling 6513869839 (letter B): a sys_* action is reachable over MCP only when the action itself declares ai: { exposed: true, description } (ADR-0011). Every refusal keyed on the object-name prefix now yields to that flag, and to nothing else.

The card named two guards. On the tree there are four sites, and all four now ask one predicate:

Site File Before After
run_action (runtime) action-execution.ts invokeBusinessAction isSystemObjectName refused every sys_* action, ahead of the exposure check systemObjectActionExposureError: refuses only when ai.exposed !== true
list_actions (runtime bridge) domains/mcp.ts listActions skipped every sys_* object unconditionally same predicate
resume_run admission (runtime bridge) domains/mcp.ts resumeAdmissionRefusal refused every sys_* action, 403 same predicate, still 403
list_actions / run_action (tool layer) mcp-http-tools.ts allowSystemObjects off ⇒ hidden / refused by name listed when the bridge attests aiExposed: true; run_action on an explicit sys_* objectName is admitted only when list_actions would list that action for this caller
  • The runtime bridge (domains/mcp.ts) had its own unconditional sys_* skip in the listing, which the card did not name; without changing it list_actions could never show a sys_* action. The seat added this file to the claim's surface (amendment 6514100238).
  • resume_run is an inference beyond the ruling's text, stated here for the contract review. resume_run is documented as admitted "by the same gates as run_action" (resumeAdmissionRefusal, MCP run_action on a screen flow dead-ends: the screen pauses even when every input is bound, and list_actions never surfaces flow input variables #15705), and its own sys_* refusal was a copy of run_action's. Leaving it name-keyed would let an agent start an exposed sys_* screen flow with run_action and then be refused on the screen it was handed. So it takes the same predicate. Nothing else in the resume gate changes.
  • McpActionSummary.aiExposed (optional) is new. The tool layer never sees a declaration, so the bridge attests the author's opt-in on each listed summary; the runtime's summarizeAction sets it only when ai.exposed === true. A bridge that does not set it keeps every sys_* action hidden and refused at the tool layer, exactly as before (fail-closed for third-party bridges of the published registerActionTools).
  • The stored-metadata family stays closed, flagged or not. The full runtime suite showed that the name refusal also carried a second boundary: stored-metadata-body-reads.test.ts ([Decision] security(runtime): may an app-authored body still READ the stored-metadata family's tables, served projected, or is that refused too (#21454 option C) #21594) pins that the MCP door never reaches the subject load for sys_metadata / sys_metadata_history, whose rows hold stored metadata bodies with credential material, even for an ai.exposed action. The ruling opened identity actions and named no family table, so the predicate keeps every isStoredMetadataBodyObject table refused before the subject load ("…stores metadata bodies and is never exposed via MCP — read and change those through the metadata API"). This is the one place the predicate does not yield to the flag; opening it would be its own decision.
  • Ruling parameter 1 (no deployment bypass): allowSystemObjects stays MCP-side, default false. It opens the tool layer's filter only; the runtime predicate does not read it, so even with it on an undeclared sys_* action is refused by the runtime.
  • Unchanged behind the flag: the ADR-0066 D4 capability gate, the ADR-0126 §8 activation consult, the ADR-0104 D2 param contract, the ai.requiresConfirmation gate, the subject load under the caller's RLS, and for a type: 'api' action the target endpoint's own authorization. The order of the gates in invokeBusinessAction is unchanged.
  • Ruling parameter 2: no platform sys_* action gains an ai block here; the declarations follow decision card [Decision] sys_* kernel objects, assessed as one set: which of the 58 platform identity actions may an agent ever reach (plain declaration / declaration + confirmation / human-only), executing #27681 B parameter 2 in one ruling #27900 (amendment 6513938013). The dogfood case uses a test-only declaration set for one boot.
  • Ruling parameter 3 (the audit row records the MCP surface) is NOT in this PR; it is carried by audit: an action run through MCP leaves no audit row naming its surface; ruling 6513869839 parameter 3 (#27681) needs an execution-context surface key, the audit writer and the forward headers #27908. Measured on the tree: neither action door writes an action-invocation audit row. The only per-invocation record is the console.info [action-audit] line on the script arm (which already names MCP run_action vs REST action); the api, flow and declarative-update arms write none. sys_audit_log rows are the data writes an action body performs, with no surface column, and ExecutionContext has no surface key (performedBy is set only at the OAuth /mcp door and is lost across the in-process api forward, which re-authenticates). A durable surface field needs packages/spec (an attribution key), the audit writer and the forward's headers, which is outside this card's lane. A log line is not offered as the audit row.
  • ADR-0066 D4 closing note: not in this PR (it is a Tier H path). It is carried by docs(adr-0066): D4's 2026-10-29 amendment still says api actions on sys_* objects stay off MCP; close it with a dated note once #27940 lands #27956, which runs after this PR merges.
  • Docs (commit 91f77a34, docs only): content/docs/ai/actions-as-tools.mdx and content/docs/ai/connect-mcp.mdx stated the old rule, that sys_* actions are held back fail-closed. They now give the per-action rule: only an action that declares ai.exposed: true itself is admitted; the stored-metadata family stays refused; every later gate is unchanged. list_actions items document aiExposed: true. The docs families derived for these two paths and pnpm docs:build all exit 0 on 91f77a34. This came from contract-review should-fix 1 (6515376004); claim amendment 6515405353.

HITL confirmation over MCP, measured before any edit

Wired end to end on origin/main e320e366: the run_action tool declares and forwards confirm (mcp-http-tools.ts), the bridge passes the whole request through (domains/mcp.ts runAction), and invokeBusinessAction calls actionConfirmationRefusal after the param contract and before the subject read and the api forward, answering 428 ACTION_CONFIRMATION_REQUIRED. Two properties the reviewer should keep in view, both pre-existing and unchanged here: the gate reads only the DECLARED ai.requiresConfirmation: true (the listing's requiresConfirmation heuristic does not gate), and confirm: true is a caller claim (decision batch #54 accepted that model: the gate makes forgetting loud, it does not prove a human). The dogfood case below shows the 428 on a sys_user action over the live transport.

Tests

  • packages/runtime/src/mcp-system-object-action-exposure.test.ts (new, 11): the predicate, including the stored-metadata family staying refused when flagged; listing lists flagged and hides unflagged, D4 still filters the listing; run_action refuses the unflagged action with and without objectName and dispatches nothing; the flagged one runs, and is refused by D4, by the activation switch (409 ACTION_DISABLED) and by the confirmation gate (428) before dispatch, then runs confirmed.
  • packages/runtime/src/mcp-resume-run.test.ts (+2): on a sys_* object a flagged flow action's run resumes; an unflagged one's run is refused 403 with the system-object refusal and the engine is not asked.
  • packages/mcp/src/mcp-action-system-object-exposure.test.ts (new, 8, through MCPServerRuntime.handleHttpRequest): attested listed / unattested hidden; a bridge that attests nothing stays closed; allowSystemObjects: true keeps its meaning; run_action refuses an unattested, an unlisted and a same-named-other-object action before the bridge runs, admits the attested one; an ordinary object is not screened.
  • packages/qa/dogfood/test/mcp-system-object-action-exposure.dogfood.test.ts (new, 6, showcase + better-auth admin + live /mcp): ban_user with a test-only ai: { exposed: true, requiresConfirmation: true }; list_actions lists it (and not unban_user); unban_user refused by name with and without objectName; unconfirmed ban_user 428 and the user stays unbanned; switched off ⇒ 409 even confirmed; a plain member's confirmed call refused; the confirmed admin call bans.
  • packages/runtime/src/http-dispatcher.test.ts: the standalone sys_api_key row pinned as refused carried ai: { exposed: true }; the ruling reverses exactly that, so the fixture is now unflagged (the flagged case lives in the new file). A moved pin, named here for review.
  • packages/runtime/src/action-execution-destructive.test.ts: the "gate 1" pin moved from isSystemObjectName to the per-action predicate (the 13 identity actions are excluded because none declares ai.exposed).

Acceptance notes

  • Branch is the card thread's platform-assigned claude/project-thread-qid1f1, not claude/issue-27681-*.
  • A run_action call on an explicit sys_* objectName now reads the bridge's listing once before running. It is the tool layer's only view of the declaration; calls on ordinary objects are not affected.
  • An exposed sys_* action the caller cannot see in its own listing (capability missing, handler unbound) is refused at the tool layer with the system-object sentence, which names both conditions, rather than with the runtime's narrower reason.
  • No ADR anchor file is added; the ruling and ADR-0011 are cited in the code at the predicate.

Verification

Ablations (each red, then restored to the HEAD blob)

Run with scripts/ablation-replace.mjs, every leg committed first; the dogfood leg went through packages/runtime/dist (ablation-dist-preflight.mjs marker present on the mutation leg, --absent after the rebuild).

Leg Mutation Red
A1 predicate no longer yields to ai.exposed runtime new file 7 red
A2 invokeBusinessAction guard deleted 2 red (unflagged refused by the wrong refusal)
A3 runtime listing back to the name-only skip 2 red
A4 resume admission back to name-only 2 red
A5 tool-layer list filter ignores aiExposed mcp new file 2 red
A6 tool-layer run_action pre-check removed 3 red
A7 summarizeAction stops attesting aiExposed 1 red
A8 A1 through dist, dogfood 4 of 6 red (list, 428, 409, confirmed ban)
A9 stored-metadata family exemption disabled 3 red (both #21594 pins + the new one)

All on head 55f4e186 (the code head; origin/main merged once at 709dba37). The final head 91f77a34 adds the docs-only commit above and no code.


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F

…ver MCP

The runtime refused every action on a sys_* object by name at the three MCP
sites (run_action, the list_actions bridge, the resume_run admission), ahead
of the ai.exposed check, and the MCP tool layer hid and refused them again.
Both now yield per action to the author's ai.exposed opt-in (ADR-0011); every
gate behind it (ADR-0066 D4 capability, ADR-0126 activation, the param
contract, ai.requiresConfirmation, the subject load) runs unchanged, and
allowSystemObjects gains no reach. The bridge attests the opt-in to the tool
layer as McpActionSummary.aiExposed.

Claude-Session: https://claude.ai/code/session_01L6bhgTcRzav55DAtus77SU
Co-authored-by: Claude <noreply@anthropic.com>
… flagged or not

The sys_* name refusal also kept the MCP door from loading a stored-metadata
row (a body carrying credential material) before the body-runner boundary;
the per-action yield keeps that table family refused.

Claude-Session: https://claude.ai/code/session_01L6bhgTcRzav55DAtus77SU
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/mcp, @objectstack/runtime, touching 11 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via aiExposed (symbol, a field of interface McpActionSummary))
  • content/docs/ai/connect-mcp.mdx (via aiExposed (symbol, a field of interface McpActionSummary))
  • content/docs/permissions/authorization.mdx (via buildMcpBridge (symbol, a top-level function))
What this run could not see
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 35 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ed0e71db94a73acc02512ff2ee0655106d02e585 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 211531ed93958127e098a7ba78f9c1f90b5971a2 — the merge of head 91f77a3413217b9efdc539dc093b794c06eda396 into base ed0e71db94a73acc02512ff2ee0655106d02e585, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 211531ed93958127e098a7ba78f9c1f90b5971a2 && git checkout 211531ed93958127e098a7ba78f9c1f90b5971a2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ed0e71db94a73acc02512ff2ee0655106d02e585 91f77a3413217b9efdc539dc093b794c06eda396 && git checkout -B drift-repro ed0e71db94a73acc02512ff2ee0655106d02e585 && git merge --no-ff 91f77a3413217b9efdc539dc093b794c06eda396

node scripts/docs-audit/affected-docs.mjs --json ed0e71db94a73acc02512ff2ee0655106d02e585

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ed0e71db94a73acc02512ff2ee0655106d02e585 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract-review-claim: session_01L6bhgUCizoYWQVJWjhdR6F · head 55f4e18 · 2026-10-30T04:44Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 55f4e1869506310aa84efcf05a513fd2654842ab
Local-runs: none
Scope: packages/runtime/src/action-execution.ts, packages/runtime/src/domains/mcp.ts, packages/mcp/src/mcp-http-tools.ts, packages/runtime/src/mcp-system-object-action-exposure.test.ts, packages/runtime/src/mcp-resume-run.test.ts, packages/runtime/src/http-dispatcher.test.ts, packages/runtime/src/action-execution-destructive.test.ts, packages/mcp/src/mcp-action-system-object-exposure.test.ts, packages/qa/dogfood/test/mcp-system-object-action-exposure.dogfood.test.ts, .changeset/27681-mcp-system-object-action-exposure.md + packages/spec/src/ui/action.zod.ts (the ai block), packages/spec/src/kernel/stored-metadata-body-objects.ts, packages/runtime/src/domains/actions.ts (the REST door), packages/runtime/src/stored-metadata-body-reads.test.ts (the #21594 pins), packages/mcp/src/index.ts, docs/adr/0066-unified-authorization-model.md (D4 and its amended note), docs/adr/0011-actions-as-ai-tools.md, content/docs/ai/actions-as-tools.mdx, content/docs/ai/connect-mcp.mdx

Ruling under review: 6513869839, letter B, maintainer 「同意」 at 2026-10-30T02:59Z, parameter 2 amended by 6513938013 (declarations to #27900), parameter 3 carried by #27908 (claim amendment 6514100238). Inputs: card #27681 and every comment, the PR body, file list, comments and net diff against main, the head's check-runs, and the cited code on the head. Adversarial reading; the dispatch order and the seat's conclusions were not inputs.

① Derived judgments

  1. The predicate is per ACTION, keyed on the action's own flag and nothing else — holds. systemObjectActionExposureError (action-execution.ts) answers null for a non-sys_* object; for a sys_* object it refuses unless action.ai.exposed === true (strict boolean). A description-only ai block, exposed: 'true', ActionAiSchema's default (exposed: z.boolean().default(false)), and any object-level or inherited value do not open it: the spec's ObjectSchema carries no ai block and no runtime path copies one onto an action (grep of object-level ai propagation on the head: none). The refusal sentence for the unflagged case keeps "is on a system object … and is not exposed via MCP", so the pre-existing pins still match it.
  2. Four sites, one predicate, no fifth path — holds. invokeBusinessAction (first gate after resolution), the runtime bridge listActions, resumeAdmissionRefusal, and the tool layer (isListableAction for list_actions and the run_action pre-check that consults the bridge's listing) all read the same fact. A grep of isSystemObjectName, isSystemObject( and the sys_ prefix regex over the head's non-test sources finds only these two files plus packages/lint (a static posture lint, not a door). The AI tool registry that materialises action_* tools is cloud-side, packages/mcp resources list no actions, and domains/ai.ts serves none. The object tools' guard (describe_object, query_records, …) keeps the name rule — objects, not actions, outside the ruling. The run_action pre-check matches on name and objectName exactly; a same-named action on another object does not admit (pinned).
  3. Behind every gate — holds; MCP is a superset of REST. invokeBusinessAction on the head runs predicate → isHeadlessInvokableAction → actionAiExposureError → ADR-0066 D4 actionPermissionError → ADR-0126 §8 disabledActionRefusal → ADR-0104 D2 enforceActionParams → HITL actionConfirmationRefusal → loadActionSubjectRecord under the caller's RLS → refuseDeniedSubjectLoad → the same-origin api forward as the caller. The diff changes only the first guard's predicate; the order is byte-for-byte the one on main. The REST door (domains/actions.ts) runs anonymous baseline → D4 → activation → params → subject load → forward, with the same shared helpers; the MCP bridge already requires a resolved principal (userId or isSystem). So no sys_* action is reachable over MCP that REST would refuse the same principal, and MCP adds the ai.exposed and confirmation gates REST does not have.
  4. Ruling parameter 1 (no deployment bypass) — holds. allowSystemObjects is read only in mcp-http-tools.ts; the runtime predicate does not read it, so with it on the tool layer lists what the bridge serves and the runtime bridge still refuses every unflagged sys_* action. No env var or option was added.
  5. resume_run inference — accepted. McpActionBridge.resumeRun's contract text and the tool description already promised run_action's gates; the name-keyed copy would have let an agent start an exposed sys_* screen flow with run_action and be refused on the screen it was handed. The principal check (trigger.userId must equal the caller's userId, run paused) sits before the admission gates and is unchanged, so a flagged run cannot be resumed by another principal (pre-existing pin), and an unflagged action's run is refused 403 with the system-object sentence before the engine is asked (new pin).
  6. Stored-metadata family — refusal list exact and justified; the flag cannot open it. STORED_METADATA_BODY_OBJECTS is the spec's one definition (sys_metadata, sys_metadata_history), consulted through isStoredMetadataBodyObject, no second list; the predicate refuses the family before it reads the flag, so ai.exposed: true on such an action is inert (pinned for every member). The [Decision] security(runtime): may an app-authored body still READ the stored-metadata family's tables, served projected, or is that refused too (#21454 option C) #21594 pins in stored-metadata-body-reads.test.ts (flagged family actions: message names the action and "system object", no record load, no handler run) still hold against the new sentence. The ruling opened identity actions and named no family table, so keeping the family closed is the conservative reading and is written at the predicate.
  7. McpActionSummary.aiExposed — additive, nothing breaks. Optional field on the published interface (exported via packages/mcp/src/index.ts); the only in-repo consumer is isListableAction. summarizeAction emits it only when ai.exposed === true, like operation; every action the runtime bridge lists already passes actionAiExposureError, so every listed summary carries it (asserted). A third-party bridge that omits it keeps every sys_* action hidden and refused, exactly as before.
  8. [Decision] sys_* kernel objects, assessed as one set: which of the 58 platform identity actions may an agent ever reach (plain declaration / declaration + confirmation / human-only), executing #27681 B parameter 2 in one ruling #27900 and audit: an action run through MCP leaves no audit row naming its surface; ruling 6513869839 parameter 3 (#27681) needs an execution-context surface key, the audit writer and the forward headers #27908 — nothing slipped in, nothing silently claimed. The diff touches no packages/platform-objects file and no audit code; the dogfood sets the test-only ai block on the in-process SysUser module before boot and restores it in afterAll, and asserts the platform declaration is still absent. The PR body states parameter 3 is not here and names audit: an action run through MCP leaves no audit row naming its surface; ruling 6513869839 parameter 3 (#27681) needs an execution-context surface key, the audit writer and the forward headers #27908; the [action-audit] log line it mentions is pre-existing and untouched.

② Semver level

@objectstack/runtime 18.2.0 → patch; @objectstack/mcp 18.2.0 → patch. Metadata-protocol: none is right (no authorable key moves: ai.exposed pre-exists; aiExposed is a wire summary field, not metadata), and none takes patch; a non-protocol widening ships patch per Post-Task Checklist step 3. Clause-②: yes (widening: …) — the arm reads as widening (readArmToken accepts the colon after the token), and the widening is real: the published McpActionSummary type and the MCP tools' accept-set grow. Changeset carries both declaration lines and the ruling reference; PR body matches. Consistent.

③ Boundary flags

④ Design judgment

  • packages/runtime/src/action-execution.ts — one exported predicate, its three runtime callers named in its doc, the family exemption citing the spec's one set, no ?? alias or shim (no ADR-0087 entry owed), and summarizeAction attesting only on the strict boolean. The doc comment's claim about gate order holds against the function body. The unused _deps matches the sibling gate signatures. Holds.
  • packages/runtime/src/domains/mcp.ts — listing and resume adopt the predicate; the isSystemObjectName import is removed with no remaining use; summarizeAction is the one producer of the attestation. Holds.
  • packages/mcp/src/mcp-http-tools.ts — isListableAction is the one predicate for list and run; the refusal sentence names both admission conditions; the header and the allowSystemObjects doc state the option's reach. The run pre-check re-lists on every sys_* run with an explicit objectName — one extra bridge call, acceptable for this class of call and cheaper than a new bridge method. Holds.
  • The six test files — each site pinned admit and refuse, gates after admission pinned, moved pins named in the PR body; the dogfood mutates in-process only, restores declaration and env, and is an isolated file. Holds.
  • .changeset/27681-mcp-system-object-action-exposure.md — both declarations, ruling reference, patch on both packages; the body states what still runs behind the flag and the day-one no-op. Holds.
  • Cited code: action.zod.ts (exposed default false, description required when exposed) is what the predicate assumes; stored-metadata-body-objects.ts is the single family source; domains/actions.ts confirms REST's gates are a subset of MCP's; ADR-0011 (opt-in default off, permission and confirmation parity) and ADR-0066 D4 (one gate, two surfaces) are honoured, D4's amended note awaiting its dated closing note.

Must-fix: none. Should-fix: (1) the two docs pages above; (2) file and name the ADR-0066 D4 follow-up card on the PR.

Implemented-by: claude/project-thread-qid1f1
Reviewed-by: session_01L6bhgUCizoYWQVJWjhdR6F

VERDICT: PASS

…he family stays refused

The two pages still stated the old rule (every sys_* action held back
fail-closed). They now state the per-action rule: an action on a sys_*
object is listed and run only on its own `ai.exposed: true`, behind every
gate it already has; the stored-metadata tables stay refused even when
flagged; and `list_actions` entries carry the `aiExposed` attestation.

Claude-Session: https://claude.ai/code/session_01L6bhgTcRzav55DAtus77SU
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 91f77a3413217b9efdc539dc093b794c06eda396
Local-runs: none
Scope: content/docs/ai/actions-as-tools.mdx, content/docs/ai/connect-mcp.mdx + packages/runtime/src/action-execution.ts (systemObjectActionExposureError, actionAiExposureError, summarizeAction, the invokeBusinessAction gate order), packages/runtime/src/domains/mcp.ts (listActions, resumeAdmissionRefusal), packages/mcp/src/mcp-http-tools.ts (isListableAction, the list_actions handler, the run_action pre-check, every allowSystemObjects read), content/docs/api/index.mdx, content/docs/permissions/system-context.mdx, content/docs/releases/v15.mdx, skills/objectstack-ai/SKILL.md

Delta review, carrying 6515376004 (PASS on 55f4e186, rendered 2026-10-30T05:00Z) to this head. The delta is one commit, 91f77a34 "docs(ai): a sys_* action is exposed over MCP on its own ai.exposed, the family stays refused": 2 files, +18 −4, both under content/docs/ai/. Inputs: the diff 55f4e186..91f77a34, the PR body as patched at 2026-10-30T05:13Z, claim amendment 6515405353 on #27681, follow-up card #27956, this head's check-runs, and the cited code read on this head. Every judgment of 6515376004 not restated below is adopted unchanged: the code tree at this head is byte-identical to the one it reviewed.

① Derived judgments

  1. No code moved — holds. git diff --stat across the delta names exactly content/docs/ai/actions-as-tools.mdx and content/docs/ai/connect-mcp.mdx; no package source, test, changeset or governed path is touched. 6515376004 ① 1–8 and ④ therefore stand as judgments of this head's code.
  2. actions-as-tools.mdx "What gets exposed" states the predicate as written — holds. The paragraph's four claims are each read off systemObjectActionExposureError on this head: (a) a sys_* action is offered "only on its own ai.exposed: true" — the predicate answers null for a sys_* object only on action?.ai?.exposed === true, a strict boolean, nothing inherited; (b) "the object's name is not a second opt-in to clear" — the name decides only that the predicate is consulted, not its answer; (c) "no deployment switch opens one that is not flagged" — allowSystemObjects is read at two sites in mcp-http-tools.ts (the object-tool guard and the action-tool filter) and nowhere in the runtime, so with it on the tool layer stops filtering but the runtime bridge still refuses every unflagged sys_* action in listActions, invokeBusinessAction and resumeAdmissionRefusal; (d) "then runs behind every gate below, as on any other object" — the gate order in invokeBusinessAction is predicate → headless-invokable → ai.exposed → ADR-0066 D4 → ADR-0126 §8 activation → ADR-0104 D2 params → confirmation → subject load under the caller's RLS → denied-load refusal → forward, unchanged from main. The "one exception" sentence matches the predicate's first branch: isStoredMetadataBodyObject (the spec's one set, sys_metadata and sys_metadata_history) is refused before the flag is read, with the refusal text sending the caller to the metadata API — the page's wording.
  3. list_actions row: aiExposed: true on every item — holds for this runtime. summarizeAction emits aiExposed: true only when ai.exposed === true; the runtime listActions pushes a summary only after actionAiExposureError (which requires the same strict true) and actionPermissionError both answer null; the tool handler returns textResult({ actions: visible, totalCount }) with the summaries unmapped, with or without allowSystemObjects. So every item a client receives from the ObjectStack runtime carries the field, and the parenthesis "the author's opt-in, attested by the bridge" is the right attribution — the tool layer never sees a declaration. A third-party bridge behind the published registerActionTools may omit it (then every sys_* action stays hidden, as before); the page documents the runtime, and the published-interface contract is stated in the mcp-http-tools.ts header, not owed on this page.
  4. connect-mcp.mdx default-exposure bullet — holds; one nit. The five gates it names behind the flag map one to one onto the code: requiredPermissions → actionPermissionError (D4); "the installation's action switch" → disabledActionRefusal (ADR-0126 §8, 409); "its param contract" → enforceActionParams (ADR-0104 D2); ai.requiresConfirmation → actionConfirmationRefusal (428); "the target's own authorization" → the same-origin api forward as the caller. The parenthesis does not name the subject load under the caller's RLS, which the sentence's "every gate behind the flag still runs" covers; a nit, not a fix. "No deployment switch opens an unflagged one", the family clause and the aiExposed: true attestation sentence are the same three facts as item 2–3. The bullet above it — objects: "except sys_* system objects, which are blocked fail-closed" — remains true of the object tools, whose guard keeps the name rule (6515376004 ① 2); the two bullets now state the two different rules for the two different tool families, which is the actual tree.
  5. connect-mcp.mdx troubleshooting row — holds. The old clause ("it targets a sys_* object (held back fail-closed for every type, api included — mcp: run_action refuses every action on a sys_* object, so an agent cannot reach the identity actions (ban_user, impersonate_user, set_user_password) that REST POST /actions serves #27681 tracks that gap)") is replaced by "it targets sys_metadata / sys_metadata_history (refused even when flagged — use the metadata API)". After the change the row's name-keyed reason is exactly the predicate's one name-keyed refusal; the other reasons in the row (ai.exposed not true, description shorter than 40 characters, non-headless type, requiredPermissions) are pre-existing and untouched.
  6. No other page or skill states the old rule — holds. git grep on this head under content/docs and skills: held back fail-closed — zero hits; #27681 — only the new sentence in actions-as-tools.mdx; sys_* and "system object" near MCP / list_actions / run_action / agent — connect-mcp.mdx line 242 (object tools, still true), content/docs/api/index.mdx line 17 ("Non-system objects exposed automatically …; actions additionally require the author's ai.exposed opt-in", true), skills/objectstack-ai/SKILL.md lines 198 and 203 (object introspection "system objects filtered out by default", true; list_actions/run_action "the same ai.exposed opt-in", true), content/docs/permissions/system-context.mdx lines 69–72 (describes isSystemObjectName / isSystemObject as helpers keyed on the sys_ name prefix — still true of both helpers on this head, which exist and key on the prefix; the page makes no claim about MCP admission), content/docs/releases/v15.mdx line 486 (a dated release note; history, not a rule). allowSystemObjects and isSystemObjectName appear in no docs or skill statement of the MCP rule. Nothing left to correct.
  7. Should-fix 1 of 6515376004 is closed by this delta (items 2–5). Should-fix 2 is closed: docs(adr-0066): D4's 2026-10-29 amendment still says api actions on sys_* objects stay off MCP; close it with a dated note once #27940 lands #27956, filed 2026-10-30T05:02Z, open, documentation, unassigned and marked not a claim, quotes the D4 amended-note sentence, asks for a dated closing note in the ADR's own amendment style after fix(mcp): an action on a sys_* object whose author exposed it to AI is listed and run over MCP, behind every gate #27940 merges, states the per-action rule and the family exception, and scopes out audit: an action run through MCP leaves no audit row naming its surface; ruling 6513869839 parameter 3 (#27681) needs an execution-context surface key, the audit writer and the forward headers #27908 and [Decision] sys_* kernel objects, assessed as one set: which of the 58 platform identity actions may an agent ever reach (plain declaration / declaration + confirmation / human-only), executing #27681 B parameter 2 in one ruling #27900. The PR body names it ("carried by docs(adr-0066): D4's 2026-10-29 amendment still says api actions on sys_* objects stay off MCP; close it with a dated note once #27940 lands #27956, which runs after this PR merges") and names the docs commit by sha with its provenance (should-fix 1, amendment 6515405353). Amendment 6515405353 on mcp: run_action refuses every action on a sys_* object, so an agent cannot reach the identity actions (ban_user, impersonate_user, set_user_password) that REST POST /actions serves #27681 adds the two pages to the claim's file surface and names docs(adr-0066): D4's 2026-10-29 amendment still says api actions on sys_* objects stay off MCP; close it with a dated note once #27940 lands #27956; the claim is otherwise unchanged.
  8. PR body on the delta — accurate, with one claim not re-run here. "Docs only", "no code", and the two pages' summary match the diff. The body's "pnpm docs:build … exit 0 on 91f77a34" is the dev's measurement; this record did not re-run it (Local-runs: none). On the head's check-runs, Check Documentation Links and Flag docs affected by code changes are success; Build Docs see ③.

② Semver level

Unchanged from 6515376004: @objectstack/runtime patch, @objectstack/mcp patch; Metadata-protocol: none; Clause-②: yes (widening: …). The delta touches no package and leaves .changeset/27681-mcp-system-object-action-exposure.md byte-identical; a docs-only commit owes no changeset. Consistent.

③ Boundary flags

  • Served-tier. record-recognisers --template --paths on the PR's 12-path committed file list at this head: no packages/spec/src schema, no governed register path (content/docs/** is not a register row), so the face is the changeset's Clause-②: yes (widening) line ⇒ the declaration face ⇒ TIER_DEFAULT, as 6515376004 found. The declaration face carries no ④; the design reading of the code is 6515376004 ④, unchanged at this head.
  • Governance. The delta adds no governed path; the PR is not a governed-surface PR before or after it. Still draft, head repo is the base repo.
  • Tracker number in docs prose. actions-as-tools.mdx now cites (#27681) beside the rule; the same page already carries (#2849) and the replaced troubleshooting row carried #27681 through a green Lint · docs & code guards on 55f4e186. Docs pages are not runtime strings; nothing owed.
  • Check-runs on this head, read at 2026-10-30T05:20Z: 53 runs — 31 success, 14 skipped, 8 in_progress, 0 failed. Green: Governed Surface Queue Guard, Build Core, Build Docs, Check Documentation Links, Flag docs affected by code changes, every Lint · leg (ESLint, docs & code guards, repo & tooling gates, CI/release/engine gates, PM dispatch-gates, the three scoped legs), every Type Check · leg (workspace, source gates, consumer gates, debt ledger), Check Changeset, PR-body guards, Spec property liveness, Dogfood Regression Gate, Test UI, Live E2E UI; the UI lint/type/build legs are path-skipped. Still running at read time: the 8 Test Core shards, which exercise the code tree this delta leaves byte-identical to 55f4e186, where all 8 were success (6515376004 ③). This record is not a landing act; the landing re-reads the head's checks.
  • Residual nits, not fixes: the connect-mcp.mdx gate parenthesis omits the RLS subject load (item 4); aiExposed is documented as always true, which is right because a non-exposed action is never listed, but a reader may expect a false arm that does not exist.
  • open_questions: none declared; none raised by this reading. Local runs: none (the shape tool's template print only).

Must-fix: none. Should-fix: none — both should-fixes of 6515376004 are closed at this head (1 by commit 91f77a34, 2 by #27956 named on the PR).

Implemented-by: claude/project-thread-qid1f1
Reviewed-by: session_01L6bhgUCizoYWQVJWjhdR6F

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Maintainer approval of the widening (recorded by the seat)

This PR declares Clause-②: yes (widening), which needs the maintainer's approval. The maintainer (Sales, the account this seat serves) gave it in the project conversation, with the reply 同意 (cmsg_01D5bWFY3rFzgJcytD5gD4UuHMrcichaVMcvHiCiTV4i3o, 2026-10-30T10:17Z).

That reply answered the seat's request to approve this PR (cmsg_01D5bWFY3rFzgJcytD5gD4UuEK7UUg8LfHYxrErPefyiPe). The request named the change: an action on a sys_* object that itself declares ai.exposed: true is listed and run over MCP, behind every gate. Just before replying, the maintainer asked whether exposing an action to AI makes it usable by everyone. The seat answered (cmsg_01D5bWFY3rFzgJcytD5gD4Uu1A7BRFg3kMdBRe4UQDZ1aN) that it does not: the caller is still an authenticated principal and passes requiredPermissions (ADR-0066 D4), activation, the param contract, confirmation and the RLS subject load, so the MCP gates are a superset of REST's (contract review 6515376004).

This record carries that approval onto the PR. It is the maintainer's word, quoted. Head at approval: 91f77a3413217b9efdc539dc093b794c06eda396. The PR is not governed (check-governed-merges.mjs --pr 27940: 0 of 12 paths on the register, 777 lines).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

维护者速读(席位终稿)

  • 改了什么:系统对象(sys_*)上的动作,只要作者在这个动作上声明了 ai: { exposed: true, description },就能经 MCP 被列出和执行。没有声明的,仍按对象名拒绝。存储元数据族(sys_metadata、sys_metadata_history)即使声明了也拒绝。
  • 为什么改:落实裁决 B(6513869839):人和 agent 用同一套系统,是否暴露按动作逐条声明,不再按对象名前缀一刀切。
  • 风险与回滚:放宽了 MCP 面。经 MCP 调用时,调用方仍需登录并通过 requiredPermissions、启用开关、参数校验、确认、按 RLS 读记录这些门,MCP 上的门是 REST 的超集。本 PR 没有给任何平台 sys_* 动作加 ai 块,那一步归 [Decision] sys_* kernel objects, assessed as one set: which of the 58 platform identity actions may an agent ever reach (plain declaration / declaration + confirmation / human-only), executing #27681 B parameter 2 in one ruling #27900。回滚 = revert 本 PR。
  • 先看:packages/runtime/src/action-execution.ts 的 systemObjectActionExposureError,以及它被调用的四处。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run src/mcp-system-object-action-exposure.test.ts。
  • 需要你做的:无。维护者已批准扩面(6520336015),由席位落地。

席位意见: ACCEPT。忠于裁决 B,四处入口用同一个谓词,后续的门都没变,契约复核和增量复核都 PASS。

席位核对

ACCEPT · head 91f77a3413217b9efdc539dc093b794c06eda396 · 2026-10-30T10:18Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

1 participant